module DCProgressBar
  @message = $dc_strings.GetString("Working ")
  @cancel_message = $dc_strings.GetString("This operation seems to be taking a long time. Would you like to continue?")

  # Define an upper limit on the time out period that
  # will be applied when the user says "Yes, keep working"
  # to the confirm dialog. Also, set a threshold before which
  # the progress bar will not show, so we're not flashing the
  # status bar if they're doing a very fast redraw. The TICK_LENGTH
  # declares the smallest time that must have passed before we show
  # another "tick" on the progress bar, so that it doesn't fill
  # the entire status bar 50 times in a single redraw.
  MAX_TIME_LIMIT = 99999.0
  DEFAULT_TIME_LIMIT = 10.0
  SHOW_BAR_AFTER = 1.0
  TICK_LENGTH = 0.05

  # Define some constants to get rid of magic numbers
  DIALOG_RESPONSE_YES = 6
  STATUS_BAR_CHAR_WIDTH = 146

  def DCProgressBar::clear()
    @status = @message
    @start_time = Time.new
    @last_tick_time = @start_time
    @time_limit = DEFAULT_TIME_LIMIT
    Sketchup::set_status_text ""
  end

  def DCProgressBar::advance(progress_bar_visible = true)
    if progress_bar_visible == true
      current_time = Time.new
      execute_time = current_time - @start_time
      if execute_time > SHOW_BAR_AFTER
        tick_time = current_time - @last_tick_time
        if tick_time > TICK_LENGTH
          @last_tick_time = current_time
          @status += "||"
          if @status.length > STATUS_BAR_CHAR_WIDTH
            @status = @message
          end
          Sketchup::set_status_text(@status)
        end
      end
      if execute_time > @time_limit
        response = UI.messagebox(@cancel_message, MB_YESNO)
        if response == DIALOG_RESPONSE_YES
          @time_limit = MAX_TIME_LIMIT
        else
          Sketchup.active_model.abort_operation
          return false
        end
      end
    end
    return true
  end
end

################################################################################
# These new methods for the Geom::Transformation class provide some live
# transformation data in a form that is compatible with the way that Dynamic
# Components think of transformations, that being rotations about axes and
# scaling along the axes. At some point, these may be rewritten
# in C++ and rolled back into the standard Ruby API.
#
#     Geom::Transformation          Methods for getting rotation and scaling
module Geom
  # These new methods for the Geom::Transformation class provide some live
  # transformation data in a form that is compatible with the way that Dynamic
  # Components think of transformations, that being rotations about axes and
  # scaling along the axes. At some point, these may be rewritten
  # in C++ and rolled back into the standard Ruby API.
  #
  # IMPORTANT NOTE: Rotation about axes makes some assumptions about the order
  # in which the three rotation transforms are applied.
  #
  # If one needs to recreate a rotation transformation, one must apply the
  # three rotations in the following order:
  #
  #   1. Apply rotx first
  #   2. Apply roty second
  #   3. Apply rotz last
  class Transformation
    # Returns the rotation about the X axis in degrees.
    #--
    # TODO(scottlininger): There are some specific states where these don't seem
    # to return correct values. It's probably some range errors in the trig
    # functions, but I'm not sure yet. One example is placing a glue to
    # component on the right side of a building and then scaling it...
    #++
    #   Args:
    #     none
    #
    #   Returns:
    #     float: rotation about X axis in degrees, rounded to 3 dec. places
    def rotx
      scale_transform = Geom::Transformation.scaling self.origin,
                                                     1.0 / self.xscale,
                                                     1.0 / self.yscale,
                                                     1.0 / self.zscale

      unscaled_trans = self * scale_transform
      a9 = self.to_a[9]
      a10 = self.to_a[10]
      return (-1 * Math.atan2(a9, a10) * (180 / Math::PI) * 1000).round / 1000
    end

    # Returns the rotation about the Y axis in degrees.
    #
    #   Args:
    #     none
    #
    #   Returns:
    #     float: rotation about Y axis in degrees, rounded to 3 dec. places
    def roty
      scale_transform = Geom::Transformation.scaling self.origin,
                                                     1.0 / self.xscale,
                                                     1.0 / self.yscale,
                                                     1.0 / self.zscale
      unscaled_trans = self * scale_transform

      a8 = unscaled_trans.to_a[8]
      if a8 > 1.0
        a8 = -1.0 + (a8 - 1.0)
      elsif a8 < -1.0
        a8 = 1.0 + (a8 + 1.0)
      end
      return (Math.asin(a8) * (180 / Math::PI) * 1000).round / 1000
    end

    # Returns the rotation about the Z axis in degrees.
    #
    #   Args:
    #     none
    #
    #   Returns:
    #     float: rotation about Z axis in degrees, rounded to 3 dec. places
    def rotz
      scale_transform = Geom::Transformation.scaling self.origin,
                                                     1.0 / self.xscale,
                                                     1.0 / self.yscale,
                                                     1.0 / self.zscale
      unscaled_trans = self * scale_transform

      a4 = unscaled_trans.to_a[4]
      a0 = unscaled_trans.to_a[0]
      return (-1 * Math.atan2(a4, a0) * (180 / Math::PI) * 1000).round / 1000
    end

    # Returns the scaling factor along the transformed entity's X axis, as a
    # multiplier. (A group that is scaled 175% along its X will return 1.75)
    #
    #   Args:
    #     none
    #
    #   Returns:
    #     xscale: float
    #--
    # TODO(scottlininger): make xscale, yscale, and zscale show negatives
    #++
    def xscale
      vector = (Geom::Vector3d.new 1, 0, 0).transform! self
      return vector.length.to_f
    end

    # Returns the scaling factor along the transformed entity's Y axis, as a
    # multiplier. (A group that is scaled 175% along its Y will return 1.75)
    #
    #   Args:
    #     none
    #
    #   Returns:
    #     yscale: float
    def yscale
      yvector = (Geom::Vector3d.new 0, 1, 0).transform! self
      return yvector.length.to_f
    end

    # Returns the scaling factor along the transformed entity's S axis, as a
    # multiplier. (A group that is scaled 175% along its Z will return 1.75)
    #
    #   Args:
    #     none
    #
    #   Returns:
    #     zscale: float
    def zscale
      vector = (Geom::Vector3d.new 0, 0, 1).transform! self
      return vector.length.to_f
    end
  end
end

module DynamicComponents
  module Util
    # @image_dir = "Plugins/su_dynamiccomponents/images/"
    # @image_dir = "D:/amtf-code/amtf-laoyu-su_dc/su_dynamiccomponents/images/"
    # @image_dir = File.join(File.dirname(File.dirname(__FILE__)), "images")
    # @image_dir = File.join($DC目录, "images")

    def self.get_image_file(basename, bitmap_postfix = "")
      if Sketchup.version.to_i > 15
        extension = Sketchup.platform == :platform_osx ? ".pdf" : ".svg"
        # filename = "#{@image_dir}#{basename}#{extension}"
        filename = File.join($Aimages目录, basename + extension)
        # puts "filename 👉 #{filename}"
        # filename 👉 Plugins/su_dynamiccomponents/images/interact_tool.svg
      else
        extension = ".png"
        filename = "#{$Aimages目录}#{basename}#{bitmap_postfix}#{extension}"
      end
      # Sketchup.find_support_file(filename)
      return filename
    end
  end # Util
end # DynamicComponents

################################################################################
# These new methods for the UI::WebDialog class provide the ability to store and
# retrieve the previous dialog size.
#
#     UI::WebDialog       Methods for getting/setting last_width and last_height
module UI

  # The following methods for the Sketchup::WebDialog object implement some
  # state maintenance capabilities.
  class WebDialog

    # Setter for last_width property. Allows us to keep track of size changes.
    def last_width=(value)
      @last_width = value.to_i
    end

    # Setter for last_height property. Allows us to keep track of size changes.
    def last_height=(value)
      @last_height = value.to_i
    end

    # Getter for last_width property.
    def last_width()
      @last_width
    end

    # Getter for last_width property.
    def last_height()
      @last_height
    end
  end
end

################################################################################
# The SketchUp module is extended by adding methods to the
# Sketchup::ComponentInstance and Sketchup::Drawingelement classes.
#
#     Sketchup::ComponentInstance   Method for making a copy of an instance
#     Sketchup::Drawingelement      Methods for getting sizes and recent scale
#
module Sketchup

  # This new method for the Sketchup::ComponentInstance class provides a copy
  # method that behaves exactly like the copy method of the Sketchup::Group
  # class (This simplifies the code when copying repeating entities, so we don't
  # need an if/then statement to handle copying Groups vs Components.
  class ComponentInstance

    # Creates a copy of the instance with the same transform as the original
    #
    #   Args:
    #     none
    #
    #   Returns:
    #     The new instance
    def copy
      self.parent.entities.add_instance self.definition, self.transformation
    end

    # Returns the description of the component's definition.
    #
    #   Args:
    #     none
    #
    #   Returns:
    #     The new instance
    def description
      self.definition.description
    end
  end

  # The following methods for the Sketchup::Drawingelement provide an easier
  # way of getting an element's "size" (lenx,leny,lenz) in both its current
  # transformation state and in its untransformed state.
  #
  # There are also methods to set and get the *last* size of an element, which
  # can then be used to return the last applied scale factor. The set_last_size
  # method is called at the end of each DynamicComponents.redraw() so we
  # have access to it in subsequent scale operations.
  #
  # There is also the all-important local_transformation method, which allows one
  # to apply transformations consistently regardless of the user's current edit
  # path.
  #--
  # TODO(scottlininger): These methods are good candidates to be baked into the
  # standard ruby API. Right now they have only been written to handle
  # ComponentInstances and Groups. They'd need some more work to work with any
  # Drawingelement, or one could instead attached these methods directly to the
  # ComponentInstance and Group classes. (Which probably makes more sense, since
  # things like faces don't have a transformation.)
  #
  # TODO(scottlininger): There must be a better way to get at the last scale
  # factor data. Ideally, it would be passed to the ToolObserver callbacks for
  # the scale tool.
  #
  # TODO(scottlininger): These methods are the only ones I can think of in the
  # Ruby API that return mutiple values. Would it be better to split them into
  # three methods that return single values? Or have them return an array of
  # values? Can C++ return multiple values? If not, these should probably be
  # changed so we have the option to bake them back into the C++ code.
  #++
  class Drawingelement

    # This new method for the Sketchup::Drawingelement class provides a way to
    # get at the "local" transformation of a ComponentInstance or Group regardless
    # of whether the user is in edit mode or at what level they are at edit mode.
    # Without this method, the .transformation method returns strange results when
    # the user is editing a component.
    def local_transformation

      # Since the internal definition.edit_transform is not exposed in the
      # current ruby API, we are using an observer to cache those
      # transformations in a global hash. See dcobservers.rb for more detail.

      # If the element we are inspecting had its local transformation cached,
      # that means that the user is editing it or its children, and we should
      # return the cached result. (As the user double clicks into components
      # or subcomponents, the transformations of each of the instances is
      # set to the identity transform by SketchUp, meaning we have no way of
      # knowing the transformation relative to its parent. Therefore, grab that
      # "local" transform from our cache.)
      if $local_edit_transforms[self.to_s] != nil
        return $local_edit_transforms[self.to_s]
      else

        # Grab the active_path, which returns an array of instances that the
        # user has "drilled into."
        active_path = Sketchup.active_model.active_path

        # Get the parent of our element.
        parent_definition = self.parent

        # If the element's parent is anywhere in the active edit path, then
        # we must correct the element's transformation as reported by the API
        # with the cached transformation of the parent.
        if active_path != nil && parent_definition != nil
          for entity in active_path
            if entity.typename == "Group"
              definition = entity
            else
              definition = entity.definition
            end
            if definition == parent_definition
              if $global_edit_transforms[entity.to_s] != nil
                return $global_edit_transforms[entity.to_s]
              end
            end
          end
        end

        # If we get this far and haven't returned, then we have no corrections
        # to make and can simply return the standard transformation.
        return Geom::Transformation.new()
      end
    end

    # 返回元素应用当前变换后的宽度、高度和深度。
    # 参数：无
    # 返回值：
    # w：浮点数，表示元素的宽度（沿 X 轴的尺寸，单位为英寸）
    # d：浮点数，表示元素的深度（沿 Y 轴的尺寸，单位为英寸）
    # h：浮点数，表示元素的高度（沿 Z 轴的尺寸，单位为英寸）
    def scaled_size
      lenx, leny, lenz = self.unscaled_size
      lenx = lenx * self.transformation.xscale
      leny = leny * self.transformation.yscale
      lenz = lenz * self.transformation.zscale
      return lenx, leny, lenz
    end

    # 返回元素在未变换状态下的宽度（lenx）、高度（lenz）和深度（leny）。
    # TODO (scottlininger)：当缩放带有引用父级尺寸公式的重复组件时，
    # 此方法会被频繁调用。若将这些值缓存到代码的其他位置，整体性能是否会更好？
    # #++
    # 参数：
    # 无
    # 返回值：
    # w：浮点数，表示宽度（沿 X 轴的尺寸，单位为英寸）
    # h：浮点数，表示高度（沿 Z 轴的尺寸，单位为英寸）
    # d：浮点数，表示深度（沿 Y 轴的尺寸，单位为英寸）
    def unscaled_size
      if self.typename == "ComponentInstance"
        db = self.definition.bounds
        lenx = db.max.x - db.min.x
        leny = db.max.y - db.min.y
        lenz = db.max.z - db.min.z
        return lenx, leny, lenz
      else
        db = self.local_bounds
        lenx = db.max.x - db.min.x
        leny = db.max.y - db.min.y
        lenz = db.max.z - db.min.z
        return lenx, leny, lenz
      end
    end

    # Stores the "last" size (lenx,leny,lenz) of the entity. This method is
    # called at the end of every DynamicComponents.redraw() so we can figure out
    # what our scaling factors are applied in subsequent scale tool
    # interactions.
    #
    #   Args:
    #     lenx: float representing the width (size along X axis in inches)
    #     leny: float representing the height (size along Y axis in inches)
    #     lenz: float representing the depth (size along Z axis in inches)
    #
    #   Returns:
    #     Nothing
    def set_last_size(lenx, leny, lenz)
      self.set_attribute @dictionary_name, "_last_lenx", lenx
      self.set_attribute @dictionary_name, "_last_leny", leny
      self.set_attribute @dictionary_name, "_last_lenz", lenz
    end

    # Returns the last scaling factors that were applied to the component, along
    # each of the three axes. These values are returned as floats, with 1.0
    # representing no scale, 2.0 representing a 2x scale, etc.
    #
    #   Args:
    #     None
    #
    #   Returns:
    #     last_xs: float representing the last scale along internal x axis
    #     last_ys: float representing the last scale along internal y axis
    #     last_zs: float representing the last scale along internal z axis
    def last_scaling_factors
      last_lenx = self.get_attribute @dictionary_name, "_last_lenx"
      last_leny = self.get_attribute @dictionary_name, "_last_leny"
      last_lenz = self.get_attribute @dictionary_name, "_last_lenz"
      if last_lenx == nil
        last_lenx, last_leny, last_lenz = self.unscaled_size
      end
      lenx, leny, lenz = self.scaled_size
      last_xs = lenx / last_lenx
      last_ys = leny / last_leny
      last_zs = lenz / last_lenz
      self.set_last_size(lenx, leny, lenz)
      return last_xs, last_ys, last_zs
    end
  end

  # The following methods for the Sketchup::Model object implement some items
  # which are already implemented by our Entity class. This simplifies
  # code such that a model can be passed into a many standard parsing routines.
  class Model

    # Returns a typename to better meld with the Drawingelement class.
    #
    #   Args:
    #     none
    #
    #   Returns:
    #     "Model"
    def typename
      return "Model"
    end

    # Returns an entityID to better meld with the Drawingelement class.
    #
    #   Args:
    #     none
    #
    #   Returns:
    #     "Model"
    def entityID
      return "Model"
    end

    # Implements delete_attribute to better meld with the Drawingelement class.
    #
    #   Args:
    #     dictionary_name  string name of the dictionary
    #     key  string name of the key to delete
    #
    #   Returns:
    #     nothing
    def delete_attribute(dictionary_name, key)
      begin
        attr_dict = self.attribute_dictionaries[dictionary_name]
        attr_dict.delete_key name
      rescue
      end
    end

    # Implements layer to better meld with the Drawingelement class.
    #
    #   Args:
    #     none
    #
    #   Returns:
    #     nothing
    def layer
      return self.layers[0]
    end
  end
end

###############################################################################
# This object implements a SKP loader that DCs consumes.
# TODO(scottlininger): The percent_done= and step= methods do not appear to be
# working in current SU7 builds, so this is only a partial implementation. Will
# need to flesh this out with a proper cancel button, etc. We will probably
# abandon this 10-second cancel button once those methods are working.
# Also, there is a "blocking" moment after the download runs for a while, where
# SU freezes up, and cancelled? stops being called.
class DCDownloader

  # Define an upper limit on the time out period that
  # will be applied when the user says "Yes, keep working"
  # to the confirm dialog. Also, set a threshold before which
  # the progress bar will not show, so we're not flashing the
  # status bar if they're doing a very fast redraw. The TICK_LENGTH
  # declares the smallest time that must have passed before we show
  # another "tick" on the progress bar, so that it doesn't fill
  # the entire status bar 50 times in a single redraw.
  MAX_TIME_LIMIT = 99999.0
  DEFAULT_TIME_LIMIT = 10.0
  TICK_LENGTH = 0.05

  # Define some constants to get rid of magic numbers
  DIALOG_RESPONSE_YES = 6
  STATUS_BAR_CHAR_WIDTH = 146

  def initialize
    @message = $dc_strings.GetString("Loading")
    @cancel_message = $dc_strings.GetString("This operation seems to be taking a long " +
                                            "time. Would you like to continue?")

    @status = @message
    @start_time = Time.new
    @last_tick_time = @start_time
    @time_limit = DEFAULT_TIME_LIMIT
    Sketchup::set_status_text ""
    @error = nil
  end

  def onPercentChange(p)
    Sketchup::set_status_text(@message + "    " + p.to_i.to_s + "%")
  end

  def cancelled?
    current_time = Time.new
    execute_time = current_time - @start_time
    if execute_time > @time_limit && @error == nil
      response = UI.messagebox(@cancel_message, MB_YESNO)
      if response == DIALOG_RESPONSE_YES
        @time_limit = MAX_TIME_LIMIT
      else
        @error = $dc_strings.GetString("Download Cancelled")
        return true
      end
    end

    return false
  end

  def onSuccess
    @error = nil
    Sketchup::set_status_text("")
  end

  def onFailure(msg)
    @error = msg
    UI.messagebox(msg)
    Sketchup::set_status_text("")
  end

  def error
    return @error
  end
end
